---
title: JWT Authentication in the GraphOS Router
subtitle: Restrict access to credentialed users and systems
description: Protect sensitive data by enabling JWT authentication in the Apollo GraphOS Router. Restrict access to credentialed users and systems.
---

<PlanRequired plans={["Free", "Developer", "Standard", "Enterprise"]}>

Rate limits apply on the Free plan.
Developer and Standard plans require Router v2.6.0 or later.

</PlanRequired>

Authentication is crucial to prevent illegitimate access and protect sensitive data in your graph. The GraphOS Router supports request authentication and key rotation via the [JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519) (**JWT**) and [JSON Web Key](https://www.rfc-editor.org/rfc/rfc7517) (**JWK**) standards. This support is compatible with popular identity providers (**IdPs**) like Okta and Auth0.

By enabling JWT authentication, you can block malicious traffic at the _edge_ of your graph instead of relying on [header forwarding](/graphos/routing/header-propagation/) to propagate tokens to your subgraphs.

<Tip>

Your subgraphs should always be accessible _only_ via the router—not directly by clients. This is especially true if you rely on JWT authentication in the router. See [Securing your subgraphs](/graphos/graphs/securing-a-subgraph) for steps to restrict subgraph access to only your router.

</Tip>

## How JWT authentication works

These are the high-level steps of JWT-based authentication with the GraphOS Router:

1. Whenever a client authenticates with your system, your IdP issues that client a valid JSON Web Token (JWT).
2. In its subsequent requests to your router, the authenticated client provides its JWT in a designated HTTP header.
3. Whenever your router receives a client request, it extracts the JWT from the designated header (if present).
   - **If no JWT is present, the request proceeds.** You can reject requests with no accompanying JWT at a later phase ([see below](#working-with-jwt-claims)).
4. Your router validates the extracted JWT using a corresponding [JSON Web Key](https://www.rfc-editor.org/rfc/rfc7517) (**JWK**).

   - Your router obtains all of its known JWKs from URLs that you specify in its configuration file. Each URL provides its keys within a single JSON object called a [JWK Set](https://www.rfc-editor.org/rfc/rfc7517#section-5) (or a **JWKS**).
   - **If validation fails, the router rejects the request.** This can occur if the JWT is malformed, or if it's been expired for more than 60 seconds (this window accounts for synchronization issues).

5. The router extracts all **claims** from the validated JWT and includes them in the request's context (`apollo::authentication::jwt_claims`), making them available to your [router customizations](/router/customizations/overview/), such as Rhai scripts.
6. The router will insert the status of JWT processing into the request context (`apollo::authentication::jwt_status`). This status is informational and may be used for logging or debugging purposes.
7. Your customizations can handle the request differently depending on the details of the extracted claims, and/or you can propagate the claims to subgraphs to enable more granular access control.
   - For examples, [see below](#working-with-jwt-claims).

## Turning it on

If you use your own custom IdP, [advanced configuration is required](#creating-your-own-jwks-advanced).

Otherwise, if you issue JWTs via a popular third-party IdP (Auth0, Okta, PingOne, etc.), enabling JWT authentication in your router is a two step process described below.

1. Set configuration options for JWT authentication in your router's [YAML config file](/router/configuration/overview/#yaml-config-file), under the `authentication` key:

   ```yaml title="router.yaml"
   authentication:
     router:
       jwt:
         jwks: # This key is required.
           - url: https://dev-zzp5enui.us.auth0.com/.well-known/jwks.json
             issuers: # optional list of issuers
               - https://issuer.one
               - https://issuer.two
             poll_interval: <optional poll interval>
             headers: # optional list of static headers added to the HTTP request to the JWKS URL
               - name: User-Agent
                 value: router
         # These keys are optional. Default values are shown.
         header_name: Authorization
         header_value_prefix: Bearer
         on_error: Error
         # array of alternative token sources
         sources:
           - type: header
             name: X-Authorization
             value_prefix: Bearer
           - type: cookie
             name: authz
   ```

   These options are documented [below](#configuration-options).

2. Pass all of the following to the `router` executable on startup:

   - The path to the router's YAML configuration file (via the `--config` option)
   - The graph ref for the [GraphOS variant](/graphos/graphs/#variants) your router should use (via the `APOLLO_GRAPH_REF` environment variable)
   - A [graph API key](/graphos/api-keys/#graph-api-keys) that enables the router to authenticate with GraphOS to fetch its supergraph schema (via the `APOLLO_KEY` environment variable)

   ```bash
   APOLLO_GRAPH_REF=docs-example-graph@main APOLLO_KEY="..." ./router --config router.yaml
   ```

When the router starts up, it displays a log message that confirms which `jwks` are in use:

```
2023-02-03T14:05:28.018932Z  INFO JWT authentication using JWKSets from jwks=[{ url: "file:///router/jwks.json" }]
```

### Configuration options

The following configuration options are supported:

<table class="field-table api-ref">
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>

<tbody>
<tr class="required">
<td style="min-width: 150px;">

##### `jwks`

</td>
<td>

**Required.** A list of JWK Set (JWKS) configuration options:

- `url`: **required** URL from which the JWKS file will be read. Must be a valid URL.
  - **If you use a third-party IdP,** consult its documentation to determine its JWKS URL.
  - **If you use your own custom IdP,** you need to make its JWKS available at a router-accessible URL if you haven't already. For more information, see [Creating your own JWKS](#creating-your-own-jwks-advanced).
- `issuers`: **optional** list of issuers accepted, that will be compared to the `iss` claim in the JWT if present. If none match, the request will be rejected.
- `algorithms`: **optional** list of accepted algorithms. Possible values are `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `EdDSA`
- `poll_interval`: **optional** interval in human-readable format (e.g. `60s` or `1hour 30s`) at which the JWKS will be polled for changes. If not specified, the JWKS endpoint will be polled every 60 seconds.
- `headers`: **optional** a list of headers sent when downloading from the JWKS URL

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `header_name`

</td>
<td>

The name of the HTTP header that client requests will use to provide their JWT to the router. Must be a valid name for an HTTP header.

The default value is `Authorization`.

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `header_value_prefix`

</td>
<td>

The string that will always precede the JWT in the header value corresponding to [`header_name`](#header_name). This value must not include whitespace.

The default value is `Bearer`.

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `on_error`

</td>
<td>

This setting controls the behavior of the router when an error occurs during JWT validation. Possible values are `Error` (default) and `Continue`.

- `Error`: The router responds with an error when an error occurs during JWT validation.
- `Continue`: The router continues processing the request when an error occurs during JWT validation. Requests with invalid JWTs will be treated as unauthenticated.

Regardless of whether JWT authentication succeeds, the status of JWT processing is inserted into the request context (`apollo::authentication::jwt_status`). This status is informational and may be used for logging or debugging purposes.

```js
// On failure
{
    // Whether the JWT came from a header or cookie source
    type: string,
    // The name of the source's field
    name: string,
    // Error details
    error: {
        // A user-friendly error message
        message: string,
        // A machine-readable error code
        code: string,
        // The underlying reason for the error, if any
        reason: string?
    }
}

// On success
{
    // Whether the JWT came from a header or cookie source
    type: string,
    // The name of the source's field
    name: string
}
```

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `sources`

</td>
<td>

This is an array of possible token sources, as it could be provided in different headers depending on the client, or it could be stored in a cookie. If the default token source defined by the above `header_name` and `header_value_prefix` does not find the token, then each of the alternative sources is tried until one matches.

```yaml title="router.yaml"
authentication:
  router:
    jwt:
      jwks:
        - url: https://dev-zzp5enui.us.auth0.com/.well-known/jwks.json
      sources:
        - type: header
          name: X-Authorization
          value_prefix: Bearer
        - type: cookie
          name: authz
```

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `ignore_other_prefixes`

</td>
<td>

This option lets you have a mix of `Authorization` header schemes, for example, both `Basic` and `Bearer`, without requiring you to use an another header.

By default, the router responds with an error when it encounters an unknown prefix in the `Authorization` header. You must explicitly define prefixes in [`header_value_prefix`](#header_value_prefix) or [`sources`](#sources).

When `ignore_other_prefixes` is `false` (the default value), the router uses the default behavior and errors when it encounters an unknown prefix in the `Authorization` header.

If you set `ignore_other_prefixes` to `true`, the router allows requests with unknown prefixes in the `Authorization` header through and does not respond with an error when encountering one.

If you set `header_value_prefix` to an empty string, the router ignores the `ignore_other_prefixes` setting.

The default value is `false`.

</td>
</tr>

</tbody>
</table>

## Working with JWT claims

After the GraphOS Router validates a client request's JWT, it adds that token's **claims** to the request's context at this key: `apollo::authentication::jwt_claims`

> - If no JWT is present for a client request, this context value is the empty tuple, `()`.
> - If a JWT _is_ present but validation of the JWT fails,
>     - When `on_error` is set to `Error`, the router _rejects_ the request.
>     - When `on_error` is set to `Continue`, the router _continues_ processing the request, and the context value is the empty tuple, `()`.

If unauthenticated requests should be rejected, the router can be configured like this:

```yaml title="router.yaml"
authorization:
  require_authentication: true
```

**Claims** are the individual details of a JWT's scope. They might include details like the ID of the associated user, any roles assigned to that user, and the JWT's expiration time. [See the spec.](https://www.rfc-editor.org/rfc/rfc7519#section-4)

Because claims are added to the context, you can define custom logic for handling each request based on the details of its claims. You can define this logic within a Rhai script or external coprocessor at the supergraph service level (for more on these options, see [Router Customizations](/router/customizations/overview/)).

Below are 2 example [Rhai script](/graphos/routing/customization/rhai/) customizations that demonstrate actions the router can perform based on a request's claims.

### Example: Forwarding claims to subgraphs as headers

Below is an example [Rhai script](/graphos/routing/customization/rhai/) that forwards a JWT's claims to individual subgraphs via HTTP headers (one header for each claim). This enables each subgraph to define logic to handle (or potentially reject) incoming requests based on claim details. This function should be imported and run in your [`main.rhai`](#example-mainrhai) file.

<Note>

This script should be run in the router's `SubgraphService`, which executes before the router sends a subquery to an individual subgraph. [Learn more about router services.](/graphos/routing/customization/rhai#router-request-lifecycle)

</Note>

<ExpansionPanel title="Click to expand">

```rhai title="claims_forwarding.rhai"
fn process_request(request) {
    let claims = request.context[Router.APOLLO_AUTHENTICATION_JWT_CLAIMS];
    if claims ==() {
      throw #{
        status: 401
      };
    }
    // Add each claim key-value pair as a separate HTTP header.
    // Note that that claims that are not present in the JWT will be added as empty strings.
    let claim_names = ["claim_1", "claim_2", "claim_3"];
    for claim_name in claim_names {
      let claim = claims[claim_name];
      claim = if claim == () {""} else {claim};
      request.subgraph.headers[claim_name] = claim;
    }
}
```

</ExpansionPanel>

<Tip>

Explicitly listing claims and _always_ setting headers for them is strongly recommended to avoid possible security issues when [forwarding headers](/graphos/routing/header-propagation/) to subgraphs.

</Tip>

### Example: Forwarding claims to subgraphs as GraphQL extensions

Below is an example [Rhai script](/graphos/routing/customization/rhai/) that forwards a JWT's claims to individual subgraphs via GraphQL extension. This enables each subgraph to define logic to handle (or potentially reject) incoming requests based on claim details. This function should be imported and run in your [`main.rhai`](#example-mainrhai) file.

<Note>

This script should be run in the router's `SubgraphService`, which executes before the router sends a subquery to an individual subgraph. [Learn more about router services.](/graphos/routing/customization/rhai#router-request-lifecycle)

</Note>

<ExpansionPanel title="Click to expand">

```rhai title="claims_forwarding.rhai"
fn process_request(request) {
    let claims = request.context[Router.APOLLO_AUTHENTICATION_JWT_CLAIMS];
    if claims ==() {
      throw #{
        status: 401
      };
    }
    request.subgraph.body.extensions["claims"] = claims;
}
```

</ExpansionPanel>

### Example: Throwing errors for invalid claims

Below is an example [Rhai script](/graphos/routing/customization/rhai/) that throws distinct errors for different invalid JWT claim details. This function should be imported and run in your [`main.rhai`](#example-mainrhai) file.

<Note>

This script should be run in the router's `SupergraphService`, which executes before the router begins generating the query plan for an operation. [Learn more about router services.](/graphos/routing/customization/rhai#router-request-lifecycle)

</Note>

<ExpansionPanel title="Click to expand">

```rhai title="claims_validation.rhai"
fn process_request(request) {
    // Router.APOLLO_AUTHENTICATION_JWT_CLAIMS is a Rhai-scope
    // constant with value `apollo::authentication::jwt_claims`
    let claims = request.context[Router.APOLLO_AUTHENTICATION_JWT_CLAIMS];
    if claims == () || !claims.contains("iss") || claims["iss"] != "https://idp.local" {
        throw #{
            status: 401,
            message: "Unauthorized"
        };
    }
    // Happy path: We have valid claims from the correct idP.
}
```

</ExpansionPanel>

### Example `main.rhai`

In order to use the above Rhai examples, you must import them into your [`main.rhai`](/graphos/routing/customization/rhai#the-main-file) like this:

<ExpansionPanel title="Click to expand">

```rhai title="main.rhai"
import "claims_validation" as claims_validation;
import "claims_forwarding" as claims_forwarding;

fn supergraph_service(service) {
    let request_callback = |request| {
        claims_validation::process_request(request);
    };

    service.map_request(request_callback);
}

fn subgraph_service(service, subgraph) {
    let request_callback = |request| {
        claims_forwarding::process_request(request);
    };

    service.map_request(request_callback);
}
```

</ExpansionPanel>

### Claim augmentation via coprocessors

You may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can augment the claims from your JSON web tokens with [coprocessors](/router/customizations/coprocessor#how-it-works).

<ExpansionPanel title="Click to expand">

A [`RouterService` coprocessor](/router/customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to:

- receive the list of claims extracted from the JWT
- use information like the `sub` (subject) claim to look up the user in an external database or service
- insert additional data in the claims list
- return the claims list to the router

For example, if you use this [router configuration](/router/configuration/overview#yaml-config-file):

```yaml title="router.yaml"
authentication:
  router:
    jwt:
      jwks:
        - url: "file:///etc/router/jwks.json"

coprocessor:
  url: http://127.0.0.1:8081
  router:
    request:
      context: all
```

The router sends requests to the coprocessor with this format:

```json
{
  "version": 1,
  "stage": "RouterRequest",
  "control": "continue",
  "id": "d0a8245df0efe8aa38a80dba1147fb2e",
  "context": {
    "entries": {
      "apollo::authentication::jwt_claims": {
        "exp": 10000000000,
        "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a"
      }
    }
  },
  "method": "POST"
}
```

The coprocessor can then look up the user with the identifier specified in the `sub` claim and return a response with more claims:

```json
{
  "version": 1,
  "stage": "RouterRequest",
  "control": "continue",
  "id": "d0a8245df0efe8aa38a80dba1147fb2e",
  "context": {
    "entries": {
      "apollo::authentication::jwt_claims": {
        "exp": 10000000000,
        "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a",
        "scope": "profile:read profile:write"
      }
    }
  }
}
```

For more information, refer to [the coprocessor documentation](/router/customizations/coprocessor/).

</ExpansionPanel>

## Creating your own JWKS (advanced)

<Note>

- **Most third-party IdP services create and host a JSON Web Key Set (JWKS) for you.** Read this section only if you use a _custom_ IdP that doesn't publish its JWKS at a router-accessible URL.
- To be compatible with JWT authentication supported by GraphOS Router, your IdP (or whatever service issues JWTs to authenticated clients) must use one of the [signature algorithms](https://crates.io/crates/jsonwebtoken#algorithms) supported by the router.

</Note>

The GraphOS Router obtains each JSON Web Key (JWK) that it uses from the URLs that you specify via the [`jwks`](#jwks) configuration option. Each URL must provide a set of valid JWKs in a single JSON object called a JWK Set (or **JWKS**).

Consult your IdP's documentation to obtain the [JWKS URL](#jwks) to pass to your router.

To provide a JWKS to your router, configure your IdP service to do the following whenever its collection of valid JWKs changes (such as when a JWK expires or is rotated):

1. Generate a [valid JWKS object](#jwks-format) that includes the details of every JWK that the router requires to perform token validation.
2. Write the JWKS object to a location that your router can reach via a `file://` or `https://` URL.
   - ⚠️ **If _any_ of your JWKs uses a symmetric signature algorithm (such as `HS256`), always use a `file://` URL.** Symmetric signature algorithms use a shared key that should _never_ be accessible over the network.

<Tip>

Make sure the IdP is configured to perform these steps _every time_ its collection of JWKs changes.

</Tip>

### JWKS format

A JWKS is a JSON object with a single top-level property: `keys`. The value of `keys` is an array of objects that each represent a single JWK:

```json title="jwks.json"
{
  "keys": [
    {
      // These JWK properties are explained below.
      "kty": "RSA",
      "alg": "RS256",
      "kid": "abc123",
      "use": "sig",
      "n": "0vx7agoebGcQSuu...",
      "e": "AQAB"
    }
  ]
}
```

It's common for the `keys` array to contain only a _single_ JWK, or sometimes two if your IdP is in the process of rotating a key.

### JWK object reference

JWK object properties fall into two categories:

- **[Universal properties.](#universal-jwk-properties)** You include these in your JWK objects regardless of which signature algorithm you use.
- **[Algorithm-specific properties.](#algorithm-specific-properties)** You include these _only_ for JWK objects that use a corresponding signature algorithm.

#### Universal properties

These properties apply to _any_ JWK:

<table class="field-table api-ref">
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>

<tbody>
<tr>
<td style="min-width: 150px;">

##### `kty`

</td>
<td>

Short for **key type**. The high-level type of cryptographic algorithm that the JWK uses (such as `RSA`, `EC`, or `oct`).

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `alg`

</td>
<td>

Short for **algorithm**. The exact cryptographic algorithm to use with the JWK, including key size (such as `RS256` or `HS512`).

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `kid`

</td>
<td>

Short for **key identifier**. The JWK's unique identifier. Your IdP should generate each JWK's `kid` at the same time that it generates the JWK itself.

JWTs created with a particular key can include that key's identifier in their payload, which helps the router determine which JWK to use for validation.

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `use`

</td>
<td>

Indicates how a JWK is used. Spec-defined values are `sig` (signature) and `enc` (encryption).

For keys you're using to perform JWT authentication, this value should be `sig`.

</td>
</tr>

</tbody>
</table>

#### Algorithm-specific properties

##### RSA

> See also the [JWA spec](https://www.rfc-editor.org/rfc/rfc7518#section-6.3).

```json
{
  // Universal properties
  "kty": "RSA",
  "alg": "RS256",
  "kid": "abc123",

  // highlight-start
  // Algorithm-specific properties
  "n": "0vx7agoebGcQSuu...", // Shortened for readability
  "e": "AQAB"
  // highlight-end
}
```

<table class="field-table api-ref">
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>

<tbody>
<tr>
<td style="min-width: 150px;">

##### `n`

</td>
<td>

The RSA public key's modulus value, as the base64-encoded value of the unsigned integer.

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `e`

</td>
<td>

The RSA public key's exponent value, as the base64-encoded value of the unsigned integer.

This value is often `AQAB`, which is the base64 encoding for the exponent `65537`.

</td>
</tr>

</tbody>
</table>

##### EC (elliptic curve)

> See also the [JWA spec](https://www.rfc-editor.org/rfc/rfc7518#section-6.2).

```json
{
  // Universal properties
  "kty": "EC",
  "alg": "ES256",
  "kid": "afda85e09a320cf748177874592de64d",
  "use": "sig",

  // highlight-start
  // Algorithm-specific properties
  "crv": "P-256",
  "x": "opFUViwCYVZLmsbG2cJTA9uPvOF5Gg8W7uNhrcorGhI",
  "y": "bPxvCFKmlqTdEFc34OekvpviUUyelGrbi020dlgIsqo"
  // highlight-end
}
```

<table class="field-table api-ref">
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>

<tbody>
<tr>
<td style="min-width: 150px;">

##### `crv`

</td>
<td>

Indicates which cryptographic curve is used with this public key.

Spec-defined curves include:

- `P-256`
- `P-384`
- `P-521`

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `x`

</td>
<td>

The x-coordinate of the elliptic curve point for this public key, as the base64-encoded value of the coordinate's octet string representation.

</td>
</tr>

<tr>
<td style="min-width: 150px;">

##### `y`

</td>
<td>

The y-coordinate of the elliptic curve point for this public key, as the base64-encoded value of the coordinate's octet string representation.

</td>
</tr>

</tbody>
</table>

##### Symmetric key algorithms (such as HMAC)

```json
{
  // Universal properties
  "kty": "oct",
  "alg": "HS256",
  "kid": "key1",
  "use": "sig",

  // highlight-start
  // Symmetric-algorithm-specific property
  "k": "c2VjcmV0Cg" // ⚠️ This is a base64-encoded shared secret! ⚠️
  // highlight-end
}
```

<table class="field-table api-ref">
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>

<tbody>
<tr>
<td style="min-width: 150px;">

##### `k`

</td>
<td>

The value of the shared symmetric key, as the [(URL safe, without padding) base64-encoded value](https://datatracker.ietf.org/doc/html/rfc7515#section-2) of the key's octet sequence representation.

**⚠️ If your JWK uses a symmetric signature algorithm, always [provide your JWKS to the router](#jwks) via a `file://` URL!** Shared keys should never be made available over the network.

</td>
</tr>

</tbody>
</table>

### JWK matching

To match an incoming JWT with its corresponding JWK, the router proceeds through descending "specificity levels" of match criteria until it identifies the first compatible JWK from its JWK Sets:

1. The JWT and JWK match both `kid` and `alg` exactly.
2. The JWT and JWK match `kid`, and the JWT's `alg` is compatible with the JWK's `kty`.
3. The JWT and JWK match `alg` exactly.
4. The JWT's `alg` is compatible with the JWK's `kty`.

This matching strategy is necessary because some identity providers (IdPs) don't specify [`alg` or `kid`](#universal-properties) values in their JWKS. However, they _always_ specify a `kty`, because that value is required by the JWK specification.

## Forwarding JWTs to subgraphs

Because the GraphOS Router handles validating incoming JWTs, you rarely need to pass those JWTs to individual subgraphs in their entirety. Instead, you usually want to [pass JWT _claims_ to subgraphs](#example-forwarding-claims-to-subgraphs-as-headers) to enable fine-grained access control.

If you _do_ need to pass entire JWTs to subgraphs, you can do so via the GraphOS Router's general-purpose [HTTP header propagation settings](/graphos/routing/header-propagation).

## Observability

If your router enables [tracing](/router/configuration/telemetry/exporters/tracing/overview), the JWT authentication plugin has its own tracing span: `authentication_plugin`

If your router [exports metrics](/graphos/routing/observability/telemetry/metrics-exporters/overview), the JWT authentication plugin exports the `apollo.router.operations.authentication.jwt` metric. You can use the metric's `authentication.jwt.failed` attribute to count failed authentications. If the `authentication.jwt.failed` attribute is absent or `false`, the authentication succeeded.

## Additional resources

You can use the Apollo Solutions [router JWKS generator](https://github.com/apollosolutions/router-jwks-generator) to create a router configuration file for use with the authentication plugin.

<SolutionsNote />
